﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.ObjectModel;
using System.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders;

public class HeaderModelBinderProviderTest
{
    public static TheoryData<BindingSource> NonHeaderBindingSources
    {
        get
        {
            return new TheoryData<BindingSource>()
                {
                    BindingSource.Body,
                    BindingSource.Form,
                    null,
                };
        }
    }

    [Theory]
    [MemberData(nameof(NonHeaderBindingSources))]
    public void Create_WhenBindingSourceIsNotFromHeader_ReturnsNull(BindingSource source)
    {
        // Arrange
        var provider = new HeaderModelBinderProvider();
        var testBinder = Mock.Of<IModelBinder>();
        var context = GetTestModelBinderProviderContext(typeof(string));
        context.OnCreatingBinder(modelMetadata => testBinder);
        context.BindingInfo.BindingSource = source;

        // Act
        var result = provider.GetBinder(context);

        // Assert
        Assert.Null(result);
    }

    [Theory]
    [InlineData(typeof(string))]
    [InlineData(typeof(bool))]
    [InlineData(typeof(int))]
    [InlineData(typeof(DateTime))]
    [InlineData(typeof(double))]
    [InlineData(typeof(CarEnumType))]
    public void Create_WhenBindingSourceIsFromHeader_ReturnsBinder_ForSimpleTypes(Type modelType)
    {
        // Arrange
        var provider = new HeaderModelBinderProvider();
        var testBinder = Mock.Of<IModelBinder>();
        var context = GetTestModelBinderProviderContext(modelType);
        context.OnCreatingBinder(modelMetadata => testBinder);
        context.BindingInfo.BindingSource = BindingSource.Header;

        // Act
        var result = provider.GetBinder(context);

        // Assert
        var headerModelBinder = Assert.IsType<HeaderModelBinder>(result);
        Assert.Same(testBinder, headerModelBinder.InnerModelBinder);
    }

    [Theory]
    [InlineData(typeof(bool?))]
    [InlineData(typeof(int?))]
    [InlineData(typeof(DateTime?))]
    [InlineData(typeof(double?))]
    [InlineData(typeof(CarEnumType?))]
    public void Create_WhenBindingSourceIsFromHeader_ReturnsBinder_ForNullableSimpleTypes(Type modelType)
    {
        // Arrange
        var provider = new HeaderModelBinderProvider();
        var testBinder = Mock.Of<IModelBinder>();
        var context = GetTestModelBinderProviderContext(modelType);
        context.OnCreatingBinder(modelMetadata => testBinder);
        context.BindingInfo.BindingSource = BindingSource.Header;

        // Act
        var result = provider.GetBinder(context);

        // Assert
        var headerModelBinder = Assert.IsType<HeaderModelBinder>(result);
        Assert.Same(testBinder, headerModelBinder.InnerModelBinder);
    }

    [Theory]
    [InlineData(typeof(string[]))]
    [InlineData(typeof(IEnumerable<bool>))]
    [InlineData(typeof(List<byte>))]
    [InlineData(typeof(Collection<short>))]
    [InlineData(typeof(float[]))]
    [InlineData(typeof(IEnumerable<decimal>))]
    [InlineData(typeof(List<double>))]
    [InlineData(typeof(ICollection<CarEnumType>))]
    public void Create_WhenBindingSourceIsFromHeader_ReturnsBinder_ForCollectionOfSimpleTypes(Type modelType)
    {
        // Arrange
        var provider = new HeaderModelBinderProvider();
        var testBinder = Mock.Of<IModelBinder>();
        var context = GetTestModelBinderProviderContext(modelType);
        context.OnCreatingBinder(modelMetadata => testBinder);
        context.BindingInfo.BindingSource = BindingSource.Header;

        // Act
        var result = provider.GetBinder(context);

        // Assert
        var headerModelBinder = Assert.IsType<HeaderModelBinder>(result);
        Assert.Same(testBinder, headerModelBinder.InnerModelBinder);
    }

    [Theory]
    [InlineData(typeof(CustomerStruct))]
    [InlineData(typeof(IEnumerable<CustomerStruct>))]
    [InlineData(typeof(Person))]
    [InlineData(typeof(IEnumerable<Person>))]
    public void Create_WhenBindingSourceIsFromHeader_ReturnsNull_ForNonSimpleModelType(Type modelType)
    {
        // Arrange
        var provider = new HeaderModelBinderProvider();
        var testBinder = Mock.Of<IModelBinder>();
        var context = GetTestModelBinderProviderContext(modelType);
        context.OnCreatingBinder(modelMetadata => testBinder);
        context.BindingInfo.BindingSource = BindingSource.Header;

        // Act
        var result = provider.GetBinder(context);

        // Assert
        Assert.Null(result);
    }

    [Theory]
    [InlineData(typeof(ProductWithTypeConverter))]
    [InlineData(typeof(IEnumerable<ProductWithTypeConverter>))]
    [InlineData(typeof(CustomerStructWithTypeConverter))]
    [InlineData(typeof(IEnumerable<CustomerStructWithTypeConverter>))]
    public void Create_WhenBindingSourceIsFromHeader_ReturnsBinder_ForNonSimpleModelType_HavingTypeConverter(
        Type modelType)
    {
        // Arrange
        var provider = new HeaderModelBinderProvider();
        var testBinder = Mock.Of<IModelBinder>();
        var context = GetTestModelBinderProviderContext(modelType);
        context.OnCreatingBinder(modelMetadata => testBinder);
        context.BindingInfo.BindingSource = BindingSource.Header;

        // Act
        var result = provider.GetBinder(context);

        // Assert
        var headerModelBinder = Assert.IsType<HeaderModelBinder>(result);
        Assert.Same(testBinder, headerModelBinder.InnerModelBinder);
    }

    [Fact]
    public void Create_WhenBindingSourceIsFromHeader_NoInnerBinderAvailable_ReturnsNull()
    {
        // Arrange
        var provider = new HeaderModelBinderProvider();
        var context = GetTestModelBinderProviderContext(typeof(string));
        context.OnCreatingBinder(modelMetadata => null);
        context.BindingInfo.BindingSource = BindingSource.Header;

        // Act
        var result = provider.GetBinder(context);

        // Assert
        Assert.Null(result);
    }

    private TestModelBinderProviderContext GetTestModelBinderProviderContext(Type modelType)
    {
        var context = new TestModelBinderProviderContext(modelType);
        var options = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
        return context;
    }

    private enum CarEnumType
    {
        Sedan,
        Coupe
    }

    private struct CustomerStruct
    {
        public string Name { get; set; }
    }

    [TypeConverter(typeof(CanConvertFromStringConverter))]
    private struct CustomerStructWithTypeConverter
    {
        public string Name { get; set; }
    }

    private class Person
    {
        public string Name { get; set; }
    }

    [TypeConverter(typeof(CanConvertFromStringConverter))]
    private class ProductWithTypeConverter
    {
        public string Name { get; set; }
    }

    private class CanConvertFromStringConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return sourceType == typeof(string);
        }
    }
}
